Building Comparable Objects (IComparable)

The System.IComparable interface specifies a behavior that allows an object to be sorted based on some specified key. Here is the formal definition:

// This interface allows an object to specify its
// relationship between other like objects.
public interface IComparable
{
    int CompareTo(object o);
}

Note The generic version of this interface (IComparable<T>) provides a more type-safe manner to handle comparisons between objects. You’ll examine generics in Chapter 10.

Let’s assume you have a new Console Application named ComparableCar that defines the following updated Car class (notice that we have basically just added a new property to represent a unique ID for each car and a modified constructor):

public class Car
{
...
    public int CarID {get; set;}
    public Car(string name, int currSp, int id)
    {
        CurrentSpeed = currSp;
        PetName = name;
        CarID = id;
    }
...
}

Now assume you have an array of Car objects as follows:

static void Main(string[] args)
{
    Console.WriteLine("***** Fun with Object Sorting *****\n");

    // Make an array of Car objects.
    Car[] myAutos = new Car[5];
    myAutos[0] = new Car("Rusty", 80, 1);
    myAutos[1] = new Car("Mary", 40, 234);
    myAutos[2] = new Car("Viper", 40, 34);
    myAutos[3] = new Car("Mel", 40, 4);
    myAutos[4] = new Car("Chucky", 40, 5);
    
    Console.ReadLine();
}

The System.Array class defines a static method named Sort(). When you invoke this method on an array of intrinsic types (int, short, string, etc.), you are able to sort the items in the array in numeric/alphabetic order as these intrinsic data types implement IComparable. However, what if you were to send an array of Car types into the Sort() method as follows?

// Sort my cars?
Array.Sort(myAutos);

If you run this test, you would get a runtime exception, as the Car class does not support the necessary interface. When you build custom types, you can implement IComparable to allow arrays of your types to be sorted. When you flesh out the details of CompareTo(), it will be up to you to decide what the baseline of the ordering operation will be. For the Car type, the internal CarID seems to be the logical candidate:

// The iteration of the Car can be ordered
// based on the CarID.
public class Car : IComparable
{
...
    // IComparable implementation.
    int IComparable.CompareTo(object obj)
    {
        Car temp = obj as Car;
        if (temp != null)
        {
            if (this.CarID > temp.CarID)
                return 1;
            if (this.CarID < temp.CarID)
                return -1;
            else
                return 0;
        }
        else
            throw new ArgumentException("Parameter is not a Car!");
    }
}

As you can see, the logic behind CompareTo() is to test the incoming object against the current instance based on a specific point of data. The return value of CompareTo() is used to discover whether this type is less than, greater than, or equal to the object it is being compared with (see Table 9-1).

Table 9-1. CompareTo() Return Values

Compare To() Return Value Description
Any number less than zero This instance comes before the specified object in the sort order.
Zero This instance is equal to the specified object.
Any number greater than zero This instance comes after the specified object in the sort order.

We can streamline the previous implementation of CompareTo() given the fact that the C# int data type (which is just a shorthand notation for the CLR System.Int32) implements IComparable. You could implement the Car’s CompareTo() as follows:

int IComparable.CompareTo(object obj)
{
    Car temp = obj as Car;
    if (temp != null)
        return this.CarID.CompareTo(temp.CarID);
    else
        throw new ArgumentException("Parameter is not a Car!");
}

In either case, so that your Car type understands how to compare itself to like objects, you can write the following user code:

// Exercise the IComparable interface.
static void Main(string[] args)
{
    // Make an array of Car objects.
...
    // Display current array.
    Console.WriteLine("Here is the unordered set of cars:");
    foreach(Car c in myAutos)
        Console.WriteLine("{0} {1}", c.CarID, c.PetName);

    // Now, sort them using IComparable!
    Array.Sort(myAutos);
    Console.WriteLine();

    // Display sorted array.
    Console.WriteLine("Here is the ordered set of cars:");
    foreach(Car c in myAutos)
        Console.WriteLine("{0} {1}", c.CarID, c.PetName);
    Console.ReadLine();
}

Here is the output from the previous Main() method:

***** Fun with Object Sorting *****
Here is the unordered set of cars:
1 Rusty
234 Mary
34 Viper
4 Mel
5 Chucky

Here is the ordered set of cars:
1 Rusty
4 Mel
5 Chucky
34 Viper
234 Mary

Specifying Multiple Sort Orders (IComparer)

In this version of the Car type, you used the car’s ID as the base of the sort order. Another design might have used the pet name of the car as the basis of the sorting algorithm (to list cars alphabetically). Now, what if you wanted to build a Car that could be sorted by ID as well as by pet name? If this is the type of behavior you are interested in, you need to make friends with another standard interface named IComparer, defined within the System.Collections namespace as follows:

// A general way to compare two objects.
interface IComparer
{
    int Compare(object o1, object o2);
}

Note The generic version of this interface (IComparer<T>) provides a more type-safe manner to handle comparisons between objects. You’ll examine generics in Chapter 10.

Unlike the IComparable interface, IComparer is typically not implemented on the type you are trying to sort (i.e., the Car). Rather, you implement this interface on any number of helper classes, one for each sort order (pet name, car ID, etc.). Currently, the Car type already knows how to compare itself against other cars based on the internal car ID. Therefore, allowing the object user to sort an array of Car object by pet name will require an additional helper class that implements IComparer. Here’s the code (be sure to import the System.Collections namespace in the code file):

// This helper class is used to sort an array of Cars by pet name.
public class PetNameComparer : IComparer
{
    // Test the pet name of each object.
    int IComparer.Compare(object o1, object o2)
    {
        Car t1 = o1 as Car;
        Car t2 = o2 as Car;
        if(t1 != null && t2 != null)
            return String.Compare(t1.PetName, t2.PetName);
        else
            throw new ArgumentException("Parameter is not a Car!");
    }
}

The object user code is able to make use of this helper class. System.Array has a number of overloaded Sort() methods, one that just happens to take an object implementing IComparer.

static void Main(string[] args)
{
...
    // Now sort by pet name.
    Array.Sort(myAutos, new PetNameComparer());

    // Dump sorted array.
    Console.WriteLine("Ordering by pet name:");
    foreach(Car c in myAutos)
        Console.WriteLine("{0} {1}", c.CarID, c.PetName);
    ...
}

Custom Properties, Custom Sort Types

It is worth pointing out that you can make use of a custom static property in order to help the object user along when sorting your Car types by a specific data point. Assume the Car class has added a static readonly property named SortByPetName that returns an instance of an object implementing the IComparer interface (PetNameComparer, in this case):

// We now support a custom property to return
// the correct IComparer interface.
public class Car : IComparable
{
    ...
    // Property to return the SortByPetName comparer.
    public static IComparer SortByPetName
    { get { return (IComparer)new PetNameComparer(); } }
}

The object user code can now sort by pet name using a strongly associated property, rather than just “having to know” to use the stand-alone PetNameComparer class type:

// Sorting by pet name made a bit cleaner.
Array.Sort(myAutos, Car.SortByPetName);

Source Code The ComparableCar project is located under the Chapter 9 subdirectory.

Hopefully, at this point you not only understand how to define and implement your own interfaces, but also understand their usefulness. To be sure, interfaces are found within every major .NET namespace, and you will continue working with various standard interfaces over the remainder of this text.